﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CodeAnalysisRuleCollection.Maths.CSharp
{
    [ExportCodeRefactoringProvider("InlinePrimitiveMathFunctions", LanguageNames.CSharp)]
    internal class InlinePrimitiveFunctionsCodeRefactoringProvider : ICodeRefactoringProvider
    {
        public async Task<IEnumerable<CodeAction>> GetRefactoringsAsync(Document document, TextSpan span, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false) as CompilationUnitSyntax;
            var model = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);

            var invocation = root.FindToken(span.Start).Parent.AncestorsAndSelf().OfType<InvocationExpressionSyntax>().FirstOrDefault();
            if (invocation == null)
                return null;

            if (IsCallToMathAbs(model, invocation, cancellationToken))
                return new[] { CodeAction.Create("Inline Math.Abs()", c => InlineAbsAsync(document, invocation, c)) };

            else if (IsCallToMathMin(model, invocation, cancellationToken))
                return new[] { CodeAction.Create("Inline Math.Min()", c => InlineMinAsync(document, invocation, c)) };

            else if (IsCallToMathMax(model, invocation, cancellationToken))
                return new[] { CodeAction.Create("Inline Math.Max()", c => InlineMaxAsync(document, invocation, c)) };

            return null;
        }


        private async Task<Document> InlineAbsAsync(Document document, InvocationExpressionSyntax invocation, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken);
            var arg = invocation.ArgumentList.Arguments[0].Expression;

            // replace node
            var newRoot = root.ReplaceNode<SyntaxNode, ExpressionSyntax>(invocation,
                SyntaxFactory.ConditionalExpression(
                    SyntaxFactory.BinaryExpression(SyntaxKind.LessThanExpression,
                        left: arg,
                        right: SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0))
                    ),
                    SyntaxFactory.PrefixUnaryExpression(SyntaxKind.UnaryMinusExpression,
                        arg
                    ),
                    arg
                )
                    .WithAdditionalAnnotations(Formatter.Annotation)
            );

            return document.WithSyntaxRoot(newRoot);
        }

        private async Task<Document> InlineMinAsync(Document document, InvocationExpressionSyntax invocation, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken);
            ExpressionSyntax
                arg1 = invocation.ArgumentList.Arguments[0].Expression,
                arg2 = invocation.ArgumentList.Arguments[1].Expression;

            // replace node
            var newRoot = root.ReplaceNode<SyntaxNode, ExpressionSyntax>(invocation,
                SyntaxFactory.ConditionalExpression(
                    SyntaxFactory.BinaryExpression(SyntaxKind.LessThanExpression,
                        left: arg1,
                        right: arg2
                    ),
                    arg1,
                    arg2
                )
                    .WithAdditionalAnnotations(Formatter.Annotation)
            );

            return document.WithSyntaxRoot(newRoot);
        }

        private async Task<Document> InlineMaxAsync(Document document, InvocationExpressionSyntax invocation, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken);
            ExpressionSyntax
                arg1 = invocation.ArgumentList.Arguments[0].Expression,
                arg2 = invocation.ArgumentList.Arguments[1].Expression;

            // replace node
            var newRoot = root.ReplaceNode<SyntaxNode, ExpressionSyntax>(invocation,
                SyntaxFactory.ConditionalExpression(
                    SyntaxFactory.BinaryExpression(SyntaxKind.GreaterThanExpression,
                        left: arg1,
                        right: arg2
                    ),
                    arg1,
                    arg2
                )
                    .WithAdditionalAnnotations(Formatter.Annotation)
            );

            return document.WithSyntaxRoot(newRoot);
        }


        private bool IsCallToMathAbs(SemanticModel semanticModel, InvocationExpressionSyntax invocation, CancellationToken cancellationToken)
        {
            var call = invocation.Expression as MemberAccessExpressionSyntax;
            if (call == null)
                return false;

            var methodSymbol = semanticModel.GetSymbolInfo(call, cancellationToken).Symbol
                as IMethodSymbol;

            if (methodSymbol == null ||
                methodSymbol.Name != "Abs")
                return false;

            var math = semanticModel.Compilation.GetTypeByMetadataName(typeof(Math).FullName);

            if (math == null ||
                !methodSymbol.ContainingType.Equals(math))
                return false;

            return true;
        }

        private bool IsCallToMathMin(SemanticModel semanticModel, InvocationExpressionSyntax invocation, CancellationToken cancellationToken)
        {
            var call = invocation.Expression as MemberAccessExpressionSyntax;
            if (call == null)
                return false;

            var methodSymbol = semanticModel.GetSymbolInfo(call, cancellationToken).Symbol
                as IMethodSymbol;

            if (methodSymbol == null ||
                methodSymbol.Name != "Min")
                return false;

            var math = semanticModel.Compilation.GetTypeByMetadataName(typeof(Math).FullName);

            if (math == null ||
                !methodSymbol.ContainingType.Equals(math))
                return false;

            return true;
        }

        private bool IsCallToMathMax(SemanticModel semanticModel, InvocationExpressionSyntax invocation, CancellationToken cancellationToken)
        {
            var call = invocation.Expression as MemberAccessExpressionSyntax;
            if (call == null)
                return false;

            var methodSymbol = semanticModel.GetSymbolInfo(call, cancellationToken).Symbol
                as IMethodSymbol;

            if (methodSymbol == null ||
                methodSymbol.Name != "Max")
                return false;

            var math = semanticModel.Compilation.GetTypeByMetadataName(typeof(Math).FullName);

            if (math == null ||
                !methodSymbol.ContainingType.Equals(math))
                return false;

            return true;
        }

        private bool IsCallToMathSign(SemanticModel semanticModel, InvocationExpressionSyntax invocation, CancellationToken cancellationToken)
        {
            var call = invocation.Expression as MemberAccessExpressionSyntax;
            if (call == null)
                return false;

            var methodSymbol = semanticModel.GetSymbolInfo(call, cancellationToken).Symbol
                as IMethodSymbol;

            if (methodSymbol == null ||
                methodSymbol.Name != "Sign")
                return false;

            var math = semanticModel.Compilation.GetTypeByMetadataName(typeof(Math).FullName);

            if (math == null ||
                !methodSymbol.ContainingType.Equals(math))
                return false;

            return true;
        }
    }
}
